<!DOCTYPE HTML>
<html>
<head>
<title>SpellTrainer/DH2013</title>
<meta http-equiv="Content-Type" content="Text/HTML; CharSet=ISO-8859-1">
<meta name="Author" content="Vasily Zakharov (vmzakhar@gmail.com)">
<link rel="Shortcut Icon" href="favicon.ico">
<style type="text/css">
html, body {
	margin: 0;
	padding: 0;
	font-family: Verdana, Arial, Helvetica, sans-serif;
	width: 100%;
	height: 100%;
}

table {
	border-spacing: 0;
	border-collapse: collapse;
}

p, td {
	font-size: 13px;
	line-height: 1.4;
	margin: 2ex 0;
	padding: 0;
}

.bold {
	font-weight: bold;
}

.light {
	font-weight: normal;
}

.right {
	text-align: right;
}

.welcome {
	color: black;
	background-color: white;
}

.ready {
	color: white;
	background-color: black;
}

.cast {
}

table.center {
	margin: 0 auto;
}

table.visible {
	display: table;
}

td.visible {
	display: table-cell;
}

.invisible {
	display: none;
}

td.menu {
	white-space: nowrap;
	font-size: 85%;
	font-weight: bold;
	color: black;
	padding: 0 2px;
}

td.column {
	vertical-align: top;
	padding: 2px 2px 5px 0px;
}

td.spell, td.category {
	white-space: nowrap;
	font-size: 75%;
	color: black;
	padding-left: 3px;
}

td.category {
	font-weight: bold;
	padding-top: 5px;
}

td.padleft {
	padding-left: 6px;
}

button.menu {
	height: 42px;
	font-weight: bold;
	line-height: normal;
}

.input {
	border: 1px inset gray;
}

span.input {
	padding: 1px;
}

input.input {
	width: 4em;
}

input.menu, select.menu {
	font-size: 100%;
	padding: 1px;
}

input.checkbox {
	border: 0;
	margin: 0;
}

img.logo {
	background-color: white;
	border: 1px solid black;
}

img.valid {
	width: 88px;
	height: 31px;
	border: 0;
}

video {
	height: 100%;
}

#headerBlockID, #selectionBlockID, #footerBlockID {
	color: black;
	background-color: silver;
	border: 1px solid black;
	height: 42px;
}

#headerTable {
	width: 100%;
}

#logoBlockID {
	padding-left: 4px;
}

#headerTitle {
	white-space: nowrap;
	text-align: center;
	color: black;
	font-weight: bold;
	padding: 0 2px 0 6px;
}

#menuPadding {
	width: 100%;
	text-align: center;
}

#noscriptTitle, #welcomeTitle {
	margin: 0;
	padding: 0;
	white-space: nowrap;
}

#loadingBlockID, #mainBlockID, #infoBlockID, #centerBlockID, #centerTable,
#welcomeBlockID, #flashBlockID, #readyBlock, #attackBlockID, #defenceBlockID, #videoBlockID {
	width: 100%;
	height: 100%;
}

#loadingBlockID, #infoBlockID, #centerBlockID, #centerTable,
#welcomeBlockID, #flashBlockID, #readyBlock, #attackBlockID, #defenceBlockID, #videoBlockID {
	text-align: center;
}

#infoBlockID, #welcomeBlockID {
	color: black;
	white-space: nowrap;
}

#startButtonID {
	width: 9ex;
}

#audioActorControlID, #videoActorControlID {
	width: 12ex;
}

#speedScrollID, #volumeScrollID {
	width: 200px;
	height: 30px;
	overflow: scroll;
}

#speedFill, #volumeFill {
	width: 1000px;
	height: 0;
}

#speedControlID, #volumeControlID {
	width: 1em;
	text-align: center;
}

#fontFamilyControlID {
	width: 5em;
}

#flashBlockID {
	color: white;
	background-color: white;
}

#readyBlockID {
	color: white;
	background-color: black;
}

#attackBlockID {
	color: green;
	background-color: black;
}

#defenceBlockID {
	color: white;
	background-color: maroon;
}

#videoBlockID {
	color: black;
	background-color: black;
}

#bigStartButton {
	font-size: 300%;
	font-weight: bold;
	cursor: pointer;
	margin: 0.5ex 0;
	padding: 0.5ex 0.5em;
}

#loggerBlockID {
	height: 38px;
	font-size: 50%;
	overflow: auto;
	text-align: left;
}

#supportedAudioID, #supportedVideoID {
	text-align: right;
	font-weight: bold;
}

</style>
<script type="text/javascript">
var LEVELS = ['Simpla', 'Maxima'];
var SERIES_LENGTHS = [3, 5];
var SERIES_DELAY = 3;
var RULE_OF_N_SPELLS = 3;

var DEFAULT_FONT_NAME = 'Verdana,Arial,Helvetica,sans-serif';
var MIN_FONT_SIZE = '20px';
var COOKIE_NAME = 'SpellTrainer/DH2013';
var GET_READY = 'Get ready!';
var NO_SPELLS = 'select spells';
var RANDOM = 'random';
var DT = 100;

var AUDIO_TYPES = {'audio/mpeg': 'mp3', 'audio/ogg': 'ogg'};
var VIDEO_TYPES = {'video/mp4': 'mp4', 'video/webm': 'webm'};
var AUDIO_ACTORS = ['Jolaf'];
var VIDEO_ACTORS = ['Aika', 'Clair', 'Hyyudu'];

var SPELLS = [
      [[null, null, [
        ['Stupefy', [1, 1], 'Enervate*'],
        ['Enervate', [1.1, 1.1], true],
        ['Silencio', [1.2, 1.2], 'Diffendo*'],
        ['Diffendo', [1.4, 1.4], true],
        ['Expelliarmus', [1.7, 1.7], 'Protego*'],
        ['Protego', [1.3, 1.3], true],
        ['Conjunctivitus', [1.2, 1.2], 'Sensus Videndi*'],
        ['Sensus Videndi', [1.3, 1.3], true],
        ['Mento Menores', [1.6, 1.6], 'Sanomento*'],
        ['Sanomento', [1.2, 1.2], true],
        ['Incarcero', [1.2, 1.2], 'Liberacio*'],
        ['Liberacio', [1.1, 1.1], true],
        ['Petrificus Totalus', [1.4, 1.4], 'Animificio*'],
        ['Animificio', [1.2, 1.2], true]
      ]]],
      [['Others', null, [
        ['Crucio', 1, ''],
        ['Sectumsempra', 1.4, '']
      ]]]
    ];

function debug(message) {
    var date = new Date().toTimeString().split(' ')[0];
    logger.innerHTML += date + '&nbsp;&nbsp;' + message + '<br>';
    logger.scrollTop = logger.scrollHeight;
    if (console) {
        console.log(date + ' ' + message);
    }
}

function getStyle(selector) {
    var rules = document.styleSheets[0].cssRules || document.styleSheets[0].rules; // trying both ways to access CSS
    for (var i = 0, rule; rule = rules[i++];) {
        if (rule.selectorText === selector) {
            return rule.style;
        }
    }
}

function SpellGenerator() {
    this.spells = [];
    this.previousSpells = [];
    this.next = function() {
        if (!this.spells.length) {
            return null;
        }
        newSpell:
        while (true) {
            spell = this.spells[Math.floor(Math.random() * this.spells.length)];
            baseSpell = spell.name.split(' ')[0];
            if (this.spells.length >= RULE_OF_N_SPELLS) {
                checkRule = true;
                if (this.spells.length <= LEVELS.length * (RULE_OF_N_SPELLS - 1)) {
                    bases = [];
                    nextSpellCheck:
                    for (var i = 0, check; check = this.spells[i++].name.split(' ')[0];) {
                        for (var j = 0, base; base = bases[j++];) {
                            if (check === base) {
                                continue nextSpellCheck;
                            }
                        }
                        bases.push(check);
                    }
                    if (bases.length < RULE_OF_N_SPELLS) {
                        checkRule = false;
                    }
                }
                if (checkRule) {
                    for (var i = 0, previousSpell; previousSpell = this.previousSpells[i++];) {
                        if (previousSpell === baseSpell) {
                            continue newSpell;
                        }
                    }
                }
            }
            this.previousSpells = this.previousSpells.concat([baseSpell]).slice(-RULE_OF_N_SPELLS + 1);
            return spell;
        }
    }
}

function toggleSelectionBlock() {
    if (selectionBlock.className === 'invisible') {
        selectionBlock.className = 'visible';
        selectionButton.innerHTML = 'Select Spells<br>&uarr; &uarr; &uarr;';
    } else {
        selectionBlock.className = 'invisible';
        selectionButton.innerHTML = 'Select Spells<br>&darr; &darr; &darr;';
    }
}

function setMode(element) {
    if (!attackControl.checked && !defenceControl.checked) {
        (element || attackControl).checked = true;
    }
}

function validateFactor(element) {
    if (!element) {
        return;
    }
    element.value = parseFloat(element.value);
    if (isNaN(element.value) || element.value < 1) {
        element.value = 1;
    } else if (element.value > 9.9) {
        element.value = 9.9;
    } else {
        element.value = Math.round(element.value * 10) / 10;
    }
}

function setSpellControl(spell, value) {
    if (spell) {
        if (spell.control) {
            spell.control.checked = value;
        }
        for (var i = 0, subSpell; subSpell = spell.inCategory[i++];) {
            setSpellControl(subSpell, value);
        }
    }
}

function checkSpellControl(spell, value) {
    if (!spell) {
        return false;
    }
    if (!spell.control) {
        return true;
    }
    if (spell.control.checked !== value) {
        return false;
    }
    for (var i = 0, subSpell; subSpell = spell.inCategory[i++];) {
        if (!checkSpellControl(subSpell, value)) {
            return false;
        }
    }
    return true;
}

function setCategoryControl(spell, value) {
    if (spell && spell.category && spell.category.control) {
        for (var i = 0, subSpell; subSpell = spell.category.inCategory[i++];) {
            if (!checkSpellControl(subSpell, value)) {
                return;
            }
        }
        spell.category.control.checked = value;
        setCategoryControl(spell.category, value);
    }
}

function setSpells(element) {
    if (element) {
        setSpellControl(element.spell, element.checked);
        setCategoryControl(element.spell, element.checked);
    }
    if (!started) {
        return;
    }
    attackGenerator.spells = [];
    defenceGenerator.spells = [];
    for (var i = 0, spell; spell = attackSpells[i++];) {
        if (spell.control.checked) {
            attackGenerator.spells.push(spell);
            if (spell.shield) {
                defenceGenerator.spells.push(spell);
            }
        }
    }
    setFont();
}

function getMediaForActor(medias, actors, actor) {
    if (actor === RANDOM) {
        var availableMedias = [];
        for (var i in medias) {
            if (medias[i]) {
                availableMedias.push(medias[i]);
            }
        }
        return availableMedias[Math.floor(Math.random() * availableMedias.length)];
    }
    return medias[actors.indexOf(actor)];
}

function playAudio(spell) {
    var audio = getMediaForActor(spell.audios, AUDIO_ACTORS, audioActor);
    if (audio) {
        audio.pause();
        audio.currentTime = 0;
        audio.volume = volume;
        audio.play();
    }
    return Boolean(audio);
}

function playVideo(spell) {
    var video = getMediaForActor(spell.videos, VIDEO_ACTORS, videoActor);
    if (video) {
        video.pause();
        video.currentTime = 0;
        videoBlock.innerHTML = '';
        videoBlock.appendChild(video);
        video.play();
    }
    return Boolean(video);
}

function castSpell() {
    var isAttack = (attackControl.checked && (!defenceControl.checked || Math.random() >= 0.5));
    var spell = (isAttack ? attackGenerator : defenceGenerator).next();
    var spellName = (spell ? spell.name : NO_SPELLS);
    var delay = spellDelay * (spell ? isAttack ? spell.factorControl.value : spell.shield.factorControl.value : 2);
    debug((isAttack ? 'Attack' : 'Defence') + ': ' + spellName + (!spell || isAttack ? '' : ' / ' + spell.shield.name) + ' (' + Math.round(delay / 10) / 100 + ')');
    if (!isAttack && spell && videoType && videoActor && playVideo(spell)) {
        setActiveBlock(videoBlock);
    } else {
        var block = (isAttack ? attackBlock : defenceBlock);
        block.firstChild.innerHTML = spellName;
        setActiveBlock(block);
    }
    if (!isAttack && spell && audioType && audioActor && volume >= 0.1) {
        playAudio(spell);
    }
    timeout = setTimeout('hideSpell()', delay);
}

function hideSpell() {
    setActiveBlock(flashBlock);
    timeout = setTimeout('castSpell()', 100);
    if (videoBlock.firstChild) {
        videoBlock.firstChild.pause();
    }
}

function setColors() {
    function doSetColors(style, field, display, control, defaultValue) {
        try { // throws exception on IE if color value is incorrect
            style[field] = display.style.backgroundColor = control.value;
        } catch(err) {
            style[field] = display.style.backgroundColor = defaultValue;
        }
    }
    doSetColors(attackStyle, 'color', attackForegroundDisplay, attackForegroundControl, 'gray');
    doSetColors(attackStyle, 'backgroundColor', attackBackgroundDisplay, attackBackgroundControl, 'white');
    doSetColors(defenceStyle, 'color', defenceForegroundDisplay, defenceForegroundControl, 'gray');
    doSetColors(defenceStyle, 'backgroundColor', defenceBackgroundDisplay, defenceBackgroundControl, 'black');
}

function setAudioActor() {
    audioActor = audioActorControl.value;
    //volumeControl.disabled = audioActor ? false : true;
    //volumeFill.style.width = audioActor ? '1000px' : 0;
    saveConfig();
}

function setVideoActor() {
    videoActor = videoActorControl.value;
    saveConfig();
}

function setSpeed() {
    speedControl.value = parseInt(speedControl.value);
    if (isNaN(speedControl.value) || speedControl.value < 1) {
        speedControl.value = 1;
    } else if (speedControl.value > 9) {
        speedControl.value = 9;
    }
    speedScroll.scrollLeft = (speedScroll.scrollWidth - speedScroll.offsetWidth) * (speedControl.value - 1) / 8;
    // calls adjustSpeed() in most cases
}

function adjustSpeed() {
    speedControl.value = 1 + (speedScroll.scrollWidth > speedScroll.offsetWidth ? Math.round(speedScroll.scrollLeft * 8 / (speedScroll.scrollWidth - speedScroll.offsetWidth)) : 0);
    spellDelay = 6000 / speedControl.value;
    saveConfig();
}

function setVolume() {
    volumeControl.value = parseInt(volumeControl.value);
    if (isNaN(volumeControl.value) || volumeControl.value < 0) {
        volumeControl.value = 0;
    } else if (volumeControl.value > 9) {
        volumeControl.value = 9;
    }
    volumeScroll.scrollLeft = (volumeScroll.scrollWidth - volumeScroll.offsetWidth) * volumeControl.value / 9;
    // calls adjustVolume() in most cases
}

function adjustVolume() {
    volumeControl.value = volumeScroll.scrollWidth > volumeScroll.offsetWidth ? Math.round(volumeScroll.scrollLeft * 9 / (volumeScroll.scrollWidth - volumeScroll.offsetWidth)) : 0;
    volume = volumeControl.value / 9;
    saveConfig();
}

function resize() {
    castStyle.fontSize = MIN_FONT_SIZE;
    castStyle.fontSize = Math.floor(Math.min(centerBlock.offsetHeight * 0.7, centerBlock.offsetWidth * aspect)) + 'px';
}

function setActiveBlock(block) {
    var ret;
    for (var i = 0, b; b = blocks[i++];) {
        if (b.className === 'visible') {
            ret = b;
        }
        b.className = (b === block) ? 'visible' : 'invisible';
    }
    return b;
}

function setFont() {
    if (!started) {
        return;
    }
    var savedBlock = setActiveBlock(flashBlock);
    var spells = attackGenerator.spells.concat(defenceGenerator.spells, [GET_READY, NO_SPELLS]);
    castStyle.fontFamily = fontFamilyControl.value + ',' + DEFAULT_FONT_NAME;
    castStyle.fontWeight = fontBoldControl.checked ? 'bold' : 'normal';
    castStyle.fontStyle = fontItalicControl.checked ? 'italic' : 'normal';
    castStyle.fontSize = MIN_FONT_SIZE;
    aspect = 1000;
    var testArea = flashBlock.firstChild;
    for (var i = 0, spell; spell = spells[i++];) {
        testArea.innerHTML = (spell.name || spell);
        aspect = Math.min(aspect, testArea.offsetHeight / testArea.offsetWidth);
    }
    testArea.innerHTML = '';
    aspect *= 0.7;
    resize();
    setActiveBlock(savedBlock);
}

function start() {
    startButton.innerHTML = 'Stop';
    startButton.onclick = stop;
    setActiveBlock(flashBlock);
    started = true;
    setSpells();
    setActiveBlock(readyBlock);
    body.onresize = onresize = resize;
    timeout = setTimeout('hideSpell()', spellDelay);
}

function stop() {
    clearTimeout(timeout);
    started = false;
    body.onresize = onresize = null;
    startButton.innerHTML = 'Start!';
    startButton.onclick = start;
    setActiveBlock(welcomeBlock);
}

function Parameter(control, defaultValue) {
    this.control = control;
    this.defaultValue = defaultValue;
    this.isSelect = (this.control.nodeName === 'SELECT');
    this.isBoolean = (typeof this.defaultValue === 'boolean');
    this.isNumber = (typeof this.defaultValue === 'number');
    this.isInteger = (this.isNumber && this.defaultValue % 1 === 0);
    this.isFloat = (this.isNumber && !this.isInteger);

    this.get = function() {
        return (this.isBoolean ? this.control.checked : this.control.value);
    }

    this.set = function(value) {
        if (this.isSelect) {
            for (var i = 0, option; option = this.control.children[i++];) {
                option.selected = (option.value === value ? 'selected' : '');
            }
        } else if (this.isBoolean) {
            this.control.checked = (value === true || value === 'true');
        } else {
            this.control.value = (this.isInteger ? parseInt(value) : this.isFloat ? parseFloat(value) : value);
        }
    }
}

function configureParameter(parameter, defaultValue) {
    var control = document.getElementById(parameter + 'ControlID');
    config[parameter] = new Parameter(control, defaultValue);
    return this[parameter + 'Control'] = control; // add to global namespace
}

function saveConfig() {
    var items = [];
    for (var name in config) {
        var parameter = config[name];
        var value = parameter.get();
        if (value != parameter.defaultValue) {
            items.push(escape(name) + '=' + escape(value));
        }
    }
    var date = new Date();
    date.setTime(date.getTime() + (items ? 365 : -1) * 24 * 60 * 60 * 1000); // 1 year
    document.cookie = COOKIE_NAME + '=' + escape(items.join(';')) + '; expires=' + date.toGMTString() + '; path=' + document.location.pathname;
}

function loadConfig(useCookie) {
    var fromCookie = {};
    if (useCookie) {
        var items = [];
        var lookFor = COOKIE_NAME + '=';
        var cookies = document.cookie.split(';');
        for (var i in cookies) {
            var cookie = cookies[i].split(' ').join('');
            if (cookie.indexOf(lookFor) === 0) {
                items = unescape(cookie.substring(lookFor.length)).split(';');
                break;
            }
        }
        for (var i in items) {
            var item = items[i].split('=');
            if (item.length === 2) {
                fromCookie[unescape(item[0].split(' ').join(''))] = unescape(item[1].split(' ').join(''));
            }
        }
    }
    for (var name in config) {
        var parameter = config[name];
        parameter.set((name in fromCookie) ? fromCookie[name] : parameter.defaultValue);
    }
    setSpells();
    setColors();
    setAudioActor();
    setVideoActor();
    setSpeed();
    adjustSpeed(); // calls saveConfig()
    setVolume();
    adjustVolume(); // calls saveConfig()
}

function createMediaName(actor, spell, extension) {
    return actor.toLowerCase() + '/' + spell.split(' ').join('') + '.' + extension;
}

function Spell(name, control, factorControl, shield, category) {
    this.setShield = function(shield) {
        this.shield = (shield === true || this.isCategory ? null : shield ? shield : (this.category && this.category.isShield) ? this.category : null);
        if (this.shield && typeof this.shield === 'object') {
            if (!this.shield.isShield) {
                this.shield.isShield = true;
                shieldSpells.push(this.shield);
            }
            this.shield.attacks.push(this);
        }
    }
    this.name = name;
    this.control = control;
    if (control) {
        control.spell = this;
    }
    this.factorControl = factorControl;
    this.category = category;
    this.isCategory = (typeof shield === 'object');
    this.isSpell = Boolean(factorControl);
    this.isAttack = Boolean(this.isSpell && !this.isCategory && control);
    this.isShield = Boolean(this.isSpell && !this.isAttack); // can be also set to true on second pass
    this.setShield(shield);
    this.attacks = [];
    this.inCategory = [];
    if (this.isSpell) {
        allSpells.push(this);
    }
    if (this.isAttack) {
        attackSpells.push(this);
        this.audios = [];
        this.videos = [];
        if (this.shield) {
            for (var i = 0, actor; actor = AUDIO_ACTORS[i++];) {
                var audio = document.createElement('audio');
                audio.src = createMediaName(actor, name, audioExtension);
                if (isDesktopBrowser) {
                    audio.volume = 0;
                    audio.play();
                } else { // can't control volume on mobiles
                    audio.load();
                }
                this.audios.push(audio);
            }
            for (var i = 0, actor; actor = VIDEO_ACTORS[i++];) {
                var video = document.createElement('video');
                var words = name.split(' ');
                if (words[words.length - 1] === 'Maxima') { // hack specific to DH2013
                    words.splice(-1, 1);
                }
                video.src = createMediaName(actor, words.join(' '), videoExtension);
                video.muted = true;
                video.play();
                this.videos.push(video);
            }
        }
    }
    if (this.isShield) {
        shieldSpells.push(this);
    }
    if (this.category) {
        category.inCategory.push(this);
    }
}

function createSpellBlock(table, data, category, on) {
    var spellName = data[0];
    var factors = data[1];
    var shield = data[2];
    var columns;
    var d = data;
    var f;
    while (true) {
        f = d[1];
        if (f && typeof f === 'object' && f.length) {
            columns = LEVELS;
            break;
        }
        d = d[2];
        if (d && typeof d === 'object' && d[0]) {
            d = d[0];
        } else {
            columns = [''];
            break;
        }
    }
    var minLevel = (factors ? columns.length : 1);
    if (factors && columns.length > 1) {
        var found = false;
        for (var i in factors) {
            if (factors[i]) {
                if (found) {
                    minLevel = i;
                    break;
                } else {
                    found = true;
                }
            }
        }
    }
    var className = (typeof shield === 'object') ? 'category' : 'spell';
    var row = table.insertRow(-1);
    var ret = [];
    var cell;
    var factor;
    for (var i in columns) {
        var column = columns[i];
        factor = (factors && typeof factors === 'object' ? factors[i] : factors);
        cell = row.insertCell(-1);
        cell.className = className + ' padleft';
        var control = null;
        if (shield != true && (!factors || factor)) {
            cell.innerHTML = '<input class="checkbox" type="checkbox" onclick="setSpells(this); saveConfig()">';
            control = cell.firstChild;
        }
        var factorControl = null;
        if (factor || column) {
            cell = row.insertCell(-1);
            cell.className = className;
            if (factor) {
                cell.innerHTML = '<input class="menu input" type="text" maxLength="3" onchange="validateFactor(this); saveConfig()">';
                factorControl = cell.firstChild;
            } else {
                cell.innerHTML = column;
            }
        }
        if (control || factorControl) {
            var fullSpellName = (!spellName ? column : (column && i >= minLevel) ? (spellName + ' ' + column) : spellName);
            var fullShield = (typeof shield === 'string') ? shield.replace('*', (column && i >= minLevel) ? (' ' + column) : '') : shield;
            var n = 0;
            while (fullSpellName + (n || '') in config) {
                n++;
            }
            fullSpellName += (n || '');
            if (control) {
                config[fullSpellName] = new Parameter(control, Boolean(on && on[i]));
            }
            if (factorControl) {
                config[fullSpellName + ' Factor'] = new Parameter(factorControl, factor);
            }
            spell = new Spell(fullSpellName, control, factorControl, fullShield, category ? category[i] : null);
            if (typeof shield === 'string' && shield) {
                spellsNeedingShields.push(spell);
            }
            ret.push(spell);
        } else {
            ret.push(null);
        }
    }
    cell = row.insertCell(-1);
    cell.className = className;
    cell.innerHTML = spellName;
    if (!spellName || !factor) {
        cell.colSpan = 2;
    }
    return ret;
}

function createSpellSection(columnBlock, section, superSectionObjects, on) {
    var sectionObjects = createSpellBlock(columnBlock, section, superSectionObjects, on);
    var subSections = section[2];
    if (subSections && typeof subSections === 'object') {
        for (var i = 0, subSection; subSection = subSections[i++];) {
            createSpellSection(columnBlock, subSection, sectionObjects, on);
        }
    }
}

function appendSelectOption(select, name) {
    var option = document.createElement('option');
    option.appendChild(document.createTextNode(name));
    select.appendChild(option);
}

function main() {
    isDesktopBrowser = navigator.userAgent.search(/(ipad)|(iphone)|(ipod)|(android)|(webos)|(mobi)|(mini)/i) < 0;
    properlyLoaded = (!isDesktopBrowser && window.opera) ? HTMLMediaElement.HAVE_CURRENT_DATA : HTMLMediaElement.HAVE_ENOUGH_DATA;
    // HTML references
    logger = document.getElementById('loggerBlockID');
    castStyle = getStyle('.cast');
    attackStyle = getStyle('#attackBlockID');
    defenceStyle = getStyle('#defenceBlockID');
    body = document.getElementsByTagName('body')[0];
    body.removeAttribute('onload'); // Don't even try to work after saving modified page
    loadingBlock = document.getElementById('loadingBlockID');
    loadingPercentage = document.getElementById('loadingPercentageID');
    mainBlock = document.getElementById('mainBlockID');
    selectionBlock = document.getElementById('selectionBlockID');
    centerBlock = document.getElementById('centerBlockID');
    welcomeBlock = document.getElementById('welcomeBlockID');
    flashBlock = document.getElementById('flashBlockID');
    readyBlock = document.getElementById('readyBlockID');
    readyBlock.firstChild.innerHTML = GET_READY;
    attackBlock = document.getElementById('attackBlockID');
    defenceBlock = document.getElementById('defenceBlockID');
    videoBlock = document.getElementById('videoBlockID');
    blocks = [welcomeBlock, flashBlock, readyBlock, attackBlock, defenceBlock, videoBlock];
    startButton = document.getElementById('startButtonID');
    selectionButton = document.getElementById('selectionButtonID');
    resetButton = document.getElementById('resetButtonID');
    spellSelector = document.getElementById('spellSelectorID');
    speedScroll = document.getElementById('speedScrollID');
    volumeScroll = document.getElementById('volumeScrollID');
    attackForegroundDisplay = document.getElementById('attackForegroundDisplayID');
    attackBackgroundDisplay = document.getElementById('attackBackgroundDisplayID');
    defenceForegroundDisplay = document.getElementById('defenceForegroundDisplayID');
    defenceBackgroundDisplay = document.getElementById('defenceBackgroundDisplayID');
    supportedAudio = document.getElementById('supportedAudioID');
    supportedVideo = document.getElementById('supportedVideoID');
    videoTag = document.getElementsByTagName('video')[0];
    // Initial settings
    timeout = null;
    allSpells = [];
    attackSpells = [];
    shieldSpells = [];
    stop();
    toggleSelectionBlock();
    attackGenerator = new SpellGenerator();
    defenceGenerator = new SpellGenerator();
    // Create config
    config = {};
    configureParameter('videoActor', RANDOM);
    configureParameter('audioActor', RANDOM);
    configureParameter('speed', 3);
    configureParameter('volume', 5);
    configureParameter('attack', true);
    configureParameter('defence', true);
    configureParameter('attackForeground', 'green');
    configureParameter('attackBackground', 'black');
    configureParameter('defenceForeground', 'white');
    configureParameter('defenceBackground', 'maroon');
    configureParameter('fontFamily', DEFAULT_FONT_NAME.split(',')[0]);
    configureParameter('fontBold', true);
    configureParameter('fontItalic', false);
    // Setting up audio and video
    audioType = audioExtension = null;
    var audio = document.createElement('audio');
    for (var type in AUDIO_TYPES) {
        if (audio.canPlayType(type)) {
            audioType = type;
            audioExtension = AUDIO_TYPES[type];
            break;
        }
    }
    if (audioType) {
        appendSelectOption(audioActorControl, RANDOM);
        for (var i = 0, actor; actor = AUDIO_ACTORS[i++];) {
          appendSelectOption(audioActorControl, actor);
        }
        supportedAudio.innerHTML = audioExtension.toUpperCase();
    } else {
        AUDIO_ACTORS = [];
        audioActorControl.disabled = true;
        volumeFill.style.width = 0;
        volumeControl.disabled = true;
    }
    videoType = videoExtension = null;
    if (isDesktopBrowser) {
        var video = document.createElement('video');
        for (var type in VIDEO_TYPES) {
            if (video.canPlayType(type)) {
                videoType = type;
                videoExtension = VIDEO_TYPES[type];
                break;
            }
        }
    }
    if (videoType) {
        appendSelectOption(videoActorControl, RANDOM);
        for (var i = 0, actor; actor = VIDEO_ACTORS[i++];) {
          appendSelectOption(videoActorControl, actor);
        }
        supportedVideo.innerHTML = videoExtension.toUpperCase();
    } else {
        VIDEO_ACTORS = [];
        videoActorControl.disabled = true;
    }
    // Configure spell selector
    spellsNeedingShields = [];
    spellSelector.deleteCell(0);
    for (var i = 0, column; column = SPELLS[i++];) {
        var cell = spellSelector.insertCell(-1);
        cell.className = 'column';
        cell.innerHTML = '<table></table>';
        var columnBlock = spellSelector.lastChild.firstChild;
        for (var j = 0, section; section = column[j++];) {
            createSpellSection(columnBlock, section, null, (i > 1) ? null : [true, false]);
        }
    }
    // Link remaining shields
    for (var i = 0, spell; spell = spellsNeedingShields[i++];) {
        for (var j = 0, shieldSpell; shieldSpell = allSpells[j++];) {
            if (shieldSpell.name === spell.shield) {
                spell.setShield(shieldSpell);
                break;
            }
        }
        if (!shieldSpell) {
            throw 'No shield found for ' + spell.name + ' ( ' + spell.shield + ')';
        }
    }
    delete allSpells;
    delete spellsNeedingShields;
    // Wait for audio and video to load
    waitForMedia();
}

function waitForMedia() {
    var ready = true;
    var loadedMedia = 0;
    var totalMedia = 0;
    for (var i = 0, spell; spell = attackSpells[i++];) {
        for (var j in spell.audios) {
            totalMedia++;
            var audio = spell.audios[j];
            if (audio && audio.readyState < properlyLoaded) {
                ready = false;
                if (audio.error) {
                    debug('ERROR loading audio from ' + audio.src);
                    spell.audios[j] = null;
                }
             } else {
                loadedMedia++;
            }
        }
        for (var j in spell.videos) {
            totalMedia++;
            var video = spell.videos[j];
            if (video && video.readyState < properlyLoaded) {
                ready = false;
                if (video.error) {
                    debug('ERROR loading video from ' + video.src);
                    spell.videos[j] = null;
                }
            } else {
                loadedMedia++;
            }
        }
    }
    if (totalMedia) {
        loadingPercentage.innerHTML = Math.round(loadedMedia * 100 / totalMedia) + '%';
    }
    if (ready) {
        // Complete the application startup
        mainComplete();
    } else {
        setTimeout(waitForMedia, DT);
    }
}

function mainComplete() {
    // Show main page
    loadingBlockID.className = 'invisible';
    mainBlock.className = 'visible';
    loadConfig(true);
    speedScroll.style.height = speedScroll.offsetHeight - speedScroll.clientHeight + 'px';
    speedScroll.onscroll = adjustSpeed;
    volumeScroll.style.height = volumeScroll.offsetHeight - volumeScroll.clientHeight + 'px';
    volumeScroll.onscroll = adjustVolume;
}
</script>
</head>
<body onload="main()">
  <table id="loadingBlockID" class="visible">
    <tr>
      <td id="infoBlockID">
        <h1 id="noscriptTitle">SpellTrainer/DH2013&nbsp;v1.3</h1>
        <p>[<a href="http://jolaf.jnm.ru/spelltrainer/dh2013.html">Latest&nbsp;version</a>] [<a href="http://jolaf.jnm.ru/spelltrainer/spelltrainer.zip">Download</a>] [<a href="http://code.google.com/p/spelltrainer/">Project&nbsp;page</a>] [<a href="mailto:vmzakhar@gmail.com?Subject=SpellTrainer">Contact&nbsp;mail</a>]</p>
        <p><img class="logo" src="wand.png" alt=""></p>
        <p>This software is created to train players in spellcasting for <a href="http://dh2013.livejournal.com/560.html">Deathly&nbsp;Hallows&nbsp;2013&nbsp;LARP</a>.</p>
        <br>
        <script type="text/javascript">document.write('<p class="bold">Loading... <span id="loadingPercentageID"></span></p>')</script>
        <noscript>
          <p class="bold">This page requires <a href="http://mozilla.org/js/">JavaScript</a> to operate.</p>
          <p class="bold">Please enable JavaScript in your browser and reload the page.</p>
        </noscript>
        <br>
        <p>If you experience problems or want to report a bug &ndash; please mail to <a href="mailto:vmzakhar@gmail.com?Subject=SpellTrainer">vmzakhar@gmail.com</a>.</p>
      </td>
    </tr>
  </table>
  <table id="mainBlockID" class="invisible">
    <tr>
      <td id="headerBlockID">
        <table id="headerTable">
          <tr>
            <td id="logoBlockID"><img class="logo" src="wand.png" alt=""></td>
            <td id="headerTitle">SpellTrainer/DH2013<br>v1.3</td>
            <td id="menuPadding" class="menu"><button id="selectionButtonID" class="menu" type="button" onclick="toggleSelectionBlock()"></button></td>
            <td class="menu"><button id="startButtonID" class="menu" type="button" onclick="start()"></button></td>
            <td>
              <table>
                <tr>
                  <td>
                    <table>
                      <tr>
                        <td class="menu padleft right">Video <select id="videoActorControlID" class="menu input" onchange="setVideoActor()"><option value="">off</option></select></td>
                        <td class="menu padleft right">Tempo <input id="speedControlID" class="menu input" type="text" maxLength="1" onchange="setSpeed()"></td>
                        <td class="menu padleft"><div id="speedScrollID"><div id="speedFill">&nbsp;</div></div></td>
                        <td class="menu padleft right">Attack</td>
                        <td class="menu"><input id="attackControlID" class="checkbox" type="checkbox" onclick="setMode(this); saveConfig()"></td>
                        <td class="menu"><input id="attackForegroundControlID" class="menu input" type="text" maxlength="20" onchange="setColors(); saveConfig()"><span id="attackForegroundDisplayID" class="input">&nbsp;&nbsp;&nbsp;</span> on <span id="attackBackgroundDisplayID" class="input">&nbsp;&nbsp;&nbsp;</span><input id="attackBackgroundControlID" class="menu input" type="text" maxlength="20" onchange="setColors(); saveConfig()"></td>
                        <td colspan="4" class="menu padleft">Font <input id="fontFamilyControlID" class="menu input" type="text" maxlength="32" onchange="setFont(); saveConfig()"></td>
                      </tr>
                      <tr>
                        <td class="menu padleft right">Audio <select id="audioActorControlID" class="menu input" onchange="setAudioActor()"><option value="">off</option></select></td>
                        <td class="menu padleft right">Volume <input id="volumeControlID" class="menu input" type="text" maxLength="1" onchange="setVolume()"></td>
                        <td class="menu padleft"><div id="volumeScrollID"><div id="volumeFill">&nbsp;</div></div></td>
                        <td class="menu padleft right">Defence</td>
                        <td class="menu"><input id="defenceControlID" class="checkbox" type="checkbox" onclick="setMode(this); saveConfig()"></td>
                        <td class="menu"><input id="defenceForegroundControlID" class="menu input" type="text" maxlength="20" onchange="setColors(); saveConfig()"><span id="defenceForegroundDisplayID" class="input">&nbsp;&nbsp;&nbsp;</span> on <span id="defenceBackgroundDisplayID" class="input">&nbsp;&nbsp;&nbsp;</span><input id="defenceBackgroundControlID" class="menu input" type="text" maxlength="20" onchange="setColors(); saveConfig()"></td>
                        <td class="menu padleft"><input id="fontBoldControlID" class="checkbox" type="checkbox" onclick="setFont(); saveConfig()"></td>
                        <td class="menu">bold</td>
                        <td class="menu"><input id="fontItalicControlID" class="checkbox" type="checkbox" onclick="setFont(); saveConfig()"></td>
                        <td class="menu">italic</td>
                      </tr>
                    </table>
                  </td>
                </tr>
              </table>
            </td>
            <td class="menu padleft"><button id="resetButtonID" class="menu" type="button" onclick="loadConfig(false)">Reset</button></td>
          </tr>
        </table>
      </td>
    </tr>
    <tr>
      <td id="selectionBlockID">
        <table id="selectionTable">
          <tr id="spellSelectorID"><td></td></tr>
        </table>
      </td>
    </tr>
    <tr>
      <td id="centerBlockID">
        <table id="centerTable">
          <tr>
            <td id="welcomeBlockID" class="visible">
              <h1 id="welcomeTitle">SpellTrainer/DH2013&nbsp;v1.3</h1>
              <p>[<a href="http://jolaf.jnm.ru/spelltrainer/dh2013.html">Latest&nbsp;version</a>] [<a href="http://jolaf.jnm.ru/spelltrainer/spelltrainer.zip">Download</a>] [<a href="http://code.google.com/p/spelltrainer/">Project&nbsp;page</a>] [<a href="mailto:vmzakhar@gmail.com?Subject=SpellTrainer">Contact&nbsp;mail</a>]</p>
              <p><img class="logo" src="wand.png" alt=""></p>
              <p>This software is created to train players in spellcasting for <a href="http://dh2013.livejournal.com/560.html">Deathly&nbsp;Hallows&nbsp;2013&nbsp;LARP</a>.</p>
              <p>Adjust the settings at the top bar as neccessary and press</p>
              <p><button id="bigStartButton" type="button" onclick="start()">Start!</button></p>
              <p class="bold">Click anywhere in the spell window to stop and return to this screen.</p>
              <p>Supported audio: <span id="supportedAudioID">None</span> &nbsp; Supported video: <span id="supportedVideoID">None</span></p>
              <p><a href="http://validator.w3.org/check?uri=referer" title="Valid HTML 4.01 Transitional"><img class="valid" src="validhtml401.png" alt="Valid HTML 4.01 Transitional"></a> <a href="http://jigsaw.w3.org/css-validator/check/referer" title="Valid CSS 2.1"><img class="valid" src="validcss.gif" alt="Valid CSS 2.1"></a></p>
            </td>
            <td id="flashBlockID" class="invisible" onclick="stop()"><span class="cast"></span></td>
            <td id="readyBlockID" class="invisible" onclick="stop()"><span class="cast"></span></td>
            <td id="attackBlockID" class="invisible" onclick="stop()"><span class="cast"></span></td>
            <td id="defenceBlockID" class="invisible" onclick="stop()"><span class="cast"></span></td>
            <td id="videoBlockID" class="invisible" onclick="stop()"></td>
          </tr>
        </table>
      </td>
    </tr>
    <tr>
      <td id="footerBlockID">
        <div id="loggerBlockID"></div>
      </td>
    </tr>
  </table>
</body>
</html>
